use std::collections::*;

use craftdig_math::{BlkCoord, ChunkCoord};

use super::map;
use super::view::WorldView;
use crate::block::{self, BlkId};
use crate::entity::{EntityId, EntityState};

/// Set of changes that can be applied onto the world.
///
/// Block granularity is on the level of whole chunks.
#[derive(Clone)]
pub struct Diff {
    pub dirty_chunks: HashMap<ChunkCoord, map::ChunkState>,
    pub dirty_entities: HashMap<EntityId, EntityState>,
}

impl Diff {
    pub fn new_empty() -> Self {
        Self {
            dirty_chunks: HashMap::new(),
            dirty_entities: HashMap::new(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.dirty_chunks.is_empty() && self.dirty_entities.is_empty()
    }

    pub fn set_chunk(&mut self, cc: ChunkCoord, chunk: map::ChunkState) {
        self.dirty_chunks.insert(cc, chunk);
    }

    pub fn is_chunk_dirty(&self, cc: ChunkCoord) -> bool {
        self.dirty_chunks.contains_key(&cc)
    }

    pub fn get_chunk(&self, cc: ChunkCoord) -> Option<&map::ChunkState> {
        self.dirty_chunks.get(&cc)
    }

    pub fn get_chunk_mut(&mut self, cc: ChunkCoord) -> Option<&mut map::ChunkState> {
        self.dirty_chunks.get_mut(&cc)
    }

    pub fn set_entity(&mut self, eid: EntityId, estate: EntityState) {
        self.dirty_entities.insert(eid, estate);
    }

    pub fn get_entity(&self, eid: &EntityId) -> Option<&EntityState> {
        self.dirty_entities.get(eid)
    }

    pub fn get_entity_mut(&mut self, eid: &EntityId) -> Option<&mut EntityState> {
        self.dirty_entities.get_mut(eid)
    }

    /// Clears pending changes to a chunk in the diff.
    pub fn reset_chunk(&mut self, cc: ChunkCoord) {
        self.dirty_chunks.remove(&cc);
    }

    /// Clears pending changes to an entity in the diff.
    pub fn reset_entity(&mut self, eid: EntityId) {
        self.dirty_entities.remove(&eid);
    }
}

/// Summary of changes that happened in a tick used to remember how to update
/// the rendered world.
pub struct TickChanges {
    /// List of chunk coordinates that were updated.
    changed_chunks: Vec<ChunkCoord>,

    /// List of entity IDs that were updated.
    entity_ids: Vec<EntityId>,
}

impl TickChanges {
    pub fn new(changed_chunks: Vec<ChunkCoord>, entity_ids: Vec<EntityId>) -> Self {
        Self {
            changed_chunks,
            entity_ids,
        }
    }

    pub fn changed_chunks(&self) -> &[ChunkCoord] {
        &self.changed_chunks
    }

    pub fn changed_entities(&self) -> &[EntityId] {
        &self.entity_ids
    }
}

/// World state with some modifications at a per-chunk level, built around some
/// other arbitrary world view.
pub struct EditableWorld<V: WorldView> {
    base: V,
    diff: Diff,
}

impl<V: WorldView> EditableWorld<V> {
    pub fn new(base: V) -> Self {
        Self {
            base,
            diff: Diff::new_empty(),
        }
    }

    /// Promotes a chunk to be edited, if not already.  If the chunk does not
    /// exist, creates one with all empty blocks.
    fn promote_chunk(&mut self, coord: ChunkCoord) {
        self.diff.dirty_chunks.entry(coord).or_insert_with(|| {
            self.base
                .get_chunk_ref(coord)
                .map(|chunk| chunk.clone())
                .unwrap_or_else(|| {
                    eprintln!("promoted new chunk {coord:?}");
                    map::ChunkState::new_full(BlkId::zero())
                })
        });
    }

    /// Promotes an entity, if it exists.
    fn promote_entity(&mut self, eid: &EntityId) -> bool {
        let Some(estate) = self.base.get_entity_state(eid) else {
            return false;
        };

        self.diff
            .dirty_entities
            .entry(*eid)
            .or_insert_with(|| estate.clone());

        true
    }

    /// Sets the state of a block, marking the chunk as dirty.
    pub fn set_block_state(&mut self, coord: BlkCoord, state: &block::BlockStateSnapshot) {
        let (cc, scp) = coord.split();
        self.promote_chunk(cc);
        let chunk = self
            .diff
            .get_chunk_mut(cc)
            .expect("tick: missing dirty chunk");
        chunk.set_block_state(scp, state);
    }

    /// Sets a whole chunk with new blocks.  Doesn't have to exist in the base
    /// state, so can be used for loading new chunks.
    pub fn set_chunk(&mut self, coord: ChunkCoord, state: map::ChunkState) {
        eprintln!("adding new chunk {coord:?}");
        self.diff.set_chunk(coord, state);
    }

    /// Sets an entity's state directly.  This can also be used to create
    /// entities.
    pub fn set_entity_state(&mut self, eid: EntityId, estate: EntityState) {
        self.diff.dirty_entities.insert(eid, estate);
    }

    pub fn into_diff(self) -> Diff {
        self.diff
    }
}

impl<V: WorldView> WorldView for EditableWorld<V> {
    fn get_block_id(&self, coord: BlkCoord) -> Option<block::BlkId> {
        let (cc, scp) = coord.split();
        if let Some(chunk) = self.diff.get_chunk(cc) {
            Some(chunk.get_block_id(scp))
        } else {
            self.base.get_block_id(coord)
        }
    }

    fn get_chunk_ref(&self, coord: ChunkCoord) -> Option<&map::ChunkState> {
        if let Some(dc) = self.diff.dirty_chunks.get(&coord) {
            Some(dc)
        } else {
            self.base.get_chunk_ref(coord)
        }
    }

    fn get_entity_state(&self, eid: &EntityId) -> Option<&EntityState> {
        if let Some(estate) = self.diff.get_entity(eid) {
            Some(estate)
        } else {
            self.base.get_entity_state(eid)
        }
    }

    fn get_entity_ids(&self) -> Vec<EntityId> {
        // TODO maybe switch over to using &impl everywhere so we can use the generic
        let mut ids = self
            .base
            .get_entity_ids()
            .into_iter()
            .collect::<HashSet<_>>();
        for e in self.diff.dirty_entities.keys() {
            ids.insert(*e);
        }

        ids.into_iter().collect::<Vec<_>>()
    }

    /*fn entity_iter(&self) -> impl Iterator<Item = &EntityId> {
            // This first yields any entities that we have that aren't present in
            // the base (meaning that they're newly added), then chains into the
            // base entity_iter.  Look ma no allocs!
            //
            // When we handle removing entities later, we can just filter them out
            // at the bottom by looking at our own diff like:
            //
            // ```
            // .filter(|e| !self.removed_entities.contains(e))
            // ```
            self.diff
                .dirty_entities
                .keys()
                .filter(|e| self.base.get_entity_state(*e).is_none())
                .chain(self.base.entity_iter());
    }*/
}

/// World view that uses a static diff to present a diffed view of a world.
///
/// Used to make it possible to maintain changing world state without extra
/// world data clones.
pub struct DiffedWorld<'w, V: WorldView> {
    base: &'w V,
    diff: &'w Diff,
}

impl<'w, V: WorldView> DiffedWorld<'w, V> {
    pub fn new(base: &'w V, diff: &'w Diff) -> Self {
        Self { base, diff }
    }
}

impl<'w, V: WorldView> WorldView for DiffedWorld<'w, V> {
    fn get_block_id(&self, coord: BlkCoord) -> Option<block::BlkId> {
        let (cc, scp) = coord.split();
        if let Some(chunk) = self.diff.get_chunk(cc) {
            Some(chunk.get_block_id(scp))
        } else {
            self.base.get_block_id(coord)
        }
    }

    fn get_chunk_ref(&self, coord: ChunkCoord) -> Option<&map::ChunkState> {
        if let Some(dc) = self.diff.get_chunk(coord) {
            Some(dc)
        } else {
            self.base.get_chunk_ref(coord)
        }
    }

    fn get_entity_state(&self, eid: &EntityId) -> Option<&EntityState> {
        if let Some(estate) = self.diff.get_entity(eid) {
            Some(estate)
        } else {
            self.base.get_entity_state(eid)
        }
    }

    fn get_entity_ids(&self) -> Vec<EntityId> {
        // TODO maybe switch over to using &impl everywhere so we can use the generic
        let mut ids = self
            .base
            .get_entity_ids()
            .into_iter()
            .collect::<HashSet<_>>();
        for e in self.diff.dirty_entities.keys() {
            ids.insert(*e);
        }

        ids.into_iter().collect::<Vec<_>>()
    }
}
